﻿#include "sqldb_utility.hpp"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <vector>

#include <mysql.h>

#ifdef _WIN32
#pragma warning(disable:4996)
#pragma comment(lib,"libmysql")
#endif


namespace sqldb{//////////////////

class mysql_db;

inline static const char* SL(const char* str) {
	const char* emptystr="";
	return str==0?emptystr:str;
}

class mysql_rs:public ResultSet{
	friend class mysql_db;
	MYSQL_RES* pRes;
	MYSQL_ROW row;
	unsigned long* plens;
	uint32_t col_ct;
protected:
	explicit mysql_rs(MYSQL_RES* p,uint32_t cc):pRes(p),row(0),plens(0),col_ct(cc) {
	}
	inline void checkNull() const {
		if(pRes==0) 
			throw GeneralException(ERROR_SQLDB_ARGUMENT_IS_EMPTY, "null recordset!");
	}
	inline void checkIdx(uint32_t idx) const {
		if (idx>col_ct) throw GeneralException(ERROR_SQLDB_ERR_IDX, "index out of range");
	}
public:
	virtual ~mysql_rs() throw() {
		if(pRes) mysql_free_result(pRes);
	}

	bool next() {
		if(pRes==0) return false;
		MYSQL_ROW r= mysql_fetch_row(pRes);
		if(r==NULL)
			return false;
		row=r;
		plens= mysql_fetch_lengths(pRes);
		///pRes->field_count;
		return true;
	}

	string get_blob(uint32_t idx) const {
		checkNull();	checkIdx(idx);
		return string(SL(row[idx]),plens[idx]);
	}

	string get_string(uint32_t idx) const {
		checkNull();	checkIdx(idx);
		return string(SL(row[idx]),plens[idx]);
	}

	int get_int(uint32_t idx) const {
		checkNull();	checkIdx(idx);
		int retv=0;
		if(sscanf(SL(row[idx]),"%d",&retv)<1)
			throw GeneralException(ERROR_SQLDB_INVALID_PARAM, "cannot get value as int!");
		return retv;
	}

	int64_t get_int64(uint32_t idx) const {
		checkNull();	checkIdx(idx);
		int64_t retv=0;
#ifdef _WIN32
		static const char* fmt="%I64d";
#else
		static const char* fmt="%ld";
#endif
		if(sscanf(SL(row[idx]),fmt,&retv)<1)
			throw GeneralException(ERROR_SQLDB_INVALID_PARAM, "cannot get value as int64!");
		return retv;
	}
	
	double get_double(uint32_t idx) const {
		checkNull();	checkIdx(idx);
		double retv=0;
		if(sscanf(SL(row[idx]),"%lf",&retv)<1)
			throw GeneralException(ERROR_SQLDB_INVALID_PARAM, "cannot get value as double!");
		return retv;
	}

	float get_float(uint32_t idx) const {
		checkNull();	checkIdx(idx);
		float retv=0;
		if(sscanf(SL(row[idx]),"%f",&retv)<1)
			throw GeneralException(ERROR_SQLDB_INVALID_PARAM, "cannot get value as float!");
		return retv;
	}

	string get_datetime(uint32_t idx) const {
		return get_string(idx);
	}
};
//=====================================
class mysql_stmt:public Statement{
	friend class mysql_db;
	string sql;
	std::vector<DbBindPara> bps;
	MYSQL_STMT* pStmt;
protected:
	explicit mysql_stmt(MYSQL_STMT*p):pStmt(p) {}
	void throwException() {
		throw GeneralException(ERROR_SQLDB_EXE_ERROR, "sqldb error");
	}
public:
	virtual ~mysql_stmt() {
		if(pStmt) mysql_stmt_close(pStmt);
	}
	
	Statement& operator << (const char* str) {
		sql+=str;
		return *this;
	}

	Statement& operator << (const string& str) {
		sql+=str;
		return *this;
	}

	Statement& operator << (const DbBindPara& bp) {
		bps.push_back(bp);	sql+="?";
		return *this;
	}

	void bind_and_exec() {
		int rt=mysql_stmt_prepare(pStmt,sql.data(),sql.size());
		if(rt) throwException();
		const long bpct=(long)mysql_stmt_param_count(pStmt);
		if (bpct>(long)bps.size()) throw GeneralException(ERROR_SQLDB_LACK_OF_BIND_PARAM, "lack of bind-parameter!");
		const static MYSQL_BIND dummy = {0};
		std::vector<MYSQL_BIND> vbinds(bpct, dummy);
		for(int i=0;i<bpct;++i) {
			MYSQL_BIND& mb=vbinds[i];	DbBindPara& bp=bps[i];
			switch(bp.data_type)
			{
			case DB_TYPE_NULL:
				mb.buffer_type=MYSQL_TYPE_NULL;
				break;
			case DB_TYPE_BLOB:
				mb.buffer_type=MYSQL_TYPE_MEDIUM_BLOB;
				mb.buffer=(void*)bp.data_ptr;
				mb.length_value=bp.data_size;
				break;
			case DB_TYPE_STRING:
				mb.buffer_type=MYSQL_TYPE_STRING;
				if(bp.data_size==(size_t)-1) {
					std::string* pstr=(std::string*)bp.data_ptr;
					mb.buffer=(void*)pstr->data();
					mb.length_value=pstr->size();
				}else {
					mb.buffer=(void*)bp.data_ptr;
					mb.length_value=bp.data_size;
				}
				break;
			case DB_TYPE_INT:
				mb.buffer_type=MYSQL_TYPE_LONG;
				mb.buffer=&bp.i_val;
				break;
			case DB_TYPE_INT64:
				mb.buffer_type=MYSQL_TYPE_LONGLONG;
				mb.buffer=&bp.i64_val;
				break;
			case DB_TYPE_FLOAT:
				mb.buffer_type=MYSQL_TYPE_FLOAT;
				mb.buffer=&bp.f_val;
				break;
			case DB_TYPE_DOUBLE:
				mb.buffer_type=MYSQL_TYPE_DOUBLE;
				mb.buffer=&bp.d_val;
				break;
			default:
				throw GeneralException(ERROR_SQLDB_INVALID_PARAM, "unhandled sql-db bind value type!");
				break;
			}
		}
		//bind & exec 
		if(vbinds.size()>0) {
			if (!mysql_stmt_bind_param(pStmt,&vbinds.front()))
				throwException();
		}
		if (mysql_stmt_execute(pStmt)) throwException();
	}
};

class _mysql_global_init{
public:
	_mysql_global_init() {
		mysql_library_init(0, NULL, NULL);
	}
	~_mysql_global_init() {
		mysql_library_end();
	}
};

static _mysql_global_init _initobj_;
//====================================
class mysql_db:public DataBase{
	MYSQL m_SQLInst;
public:
	mysql_db() {
		mysql_init(&m_SQLInst);
	}
	virtual ~mysql_db() throw() {
		mysql_close(&m_SQLInst);
	}

	inline void throwException() {
		throw GeneralException(ERROR_SQLDB_EXE_ERROR, "sqldb error");
	}

	//@override
	void connect(const string& conn_url,const string& uname,const string& pswd) {
		if(m_SQLInst.net.fd!=0) {
			throw GeneralException(ERROR_SQLDB_RECONNECT_ERROR,"should not re-connect an opened connection. Please create another instance!");
		}
		//parse url,port,dbname from 'conn_url', like "localhost:3306/my_db"
		string tmpurl(conn_url);
		size_t pos1=tmpurl.find('/');
		if(pos1==string::npos) {
			throw GeneralException(ERROR_SQLDB_INVALID_URL, "'conn_url' parse error! cannot find '/'");
		}
		tmpurl[pos1]=0;
		int port=3306;	//default value
		char* p2=strchr((char*)tmpurl.data(),':');
		if(p2!=0) {
			sscanf(p2+1,"%d",&port);
			*p2=0;
		}
		if(NULL==mysql_real_connect(&m_SQLInst,tmpurl.data(),uname.data(),pswd.data(),""/*tmpurl.data()+pos1+1*/,
			port,NULL,CLIENT_MULTI_STATEMENTS)) {
            throw GeneralException(ERROR_SQLDB_CONNECT_ERROR, "data base connect error!");
		}
		string db(tmpurl.data() + pos1 + 1);
		string createdb_sql = "Create Database If Not Exists " + db + "  Character Set UTF8";
		exec_no_query(createdb_sql);
		string usedb_sql = "USE " + db;
		exec_no_query(usedb_sql);
	}

	void exec_no_query(const string& sql) {
		int rt=mysql_real_query(&m_SQLInst,sql.data(),(unsigned long)sql.size());
		if(rt!=0) throwException();
		//mysql_affected_rows(&m_SQLInst);
	}

	shared_ptr<ResultSet> exec_query(const string& sql) {
		int rt=mysql_real_query(&m_SQLInst,sql.data(),(unsigned long)sql.size());
		if(rt!=0) throwException();
		MYSQL_RES* pr=mysql_store_result(&m_SQLInst);
		if(pr==NULL) {
			if(mysql_errno(&m_SQLInst)!=0) throwException();
		}
		return shared_ptr<ResultSet>(new mysql_rs(pr,mysql_field_count(&m_SQLInst)));
	}

	int run_ctrl_cmd(int cmd,void* para1/* =0 */,void* para2/* =0 */,void* para3/* =0 */) {
		switch(cmd) {
		case cc_ping:
			return mysql_ping(&m_SQLInst);
		case cc_affected_rows:
			*reinterpret_cast<uint64_t*>(para1) = mysql_affected_rows(&m_SQLInst);
			return 0;
		default:
			break;
		}
		return DataBase::run_ctrl_cmd(cmd);
	}

	shared_ptr<Statement> prepare_stmt() {
		MYSQL_STMT* p=mysql_stmt_init(&m_SQLInst);
		if (p == 0) throw GeneralException(ERROR_SQLDB_ALLOCATE_MYSQL_STMT_FAILED, "mysql allocate MYSQL_STMT failed!");
		return shared_ptr<Statement>(new mysql_stmt(p));
	}
};

//extern 
shared_ptr<DataBase> __create_mysql_db_inst() {
	return shared_ptr<DataBase>(new mysql_db());
}


}///////////////////////////////
